import os
import threading

include "uint_hash_table.pyx"
cdef dict cache_files={}
cdef u64 max_cache_file_count=1<<20-64
cdef open_file(str filepath):
    cdef File file
    try:
        return cache_files[filepath]
    except KeyError:
        file

cdef class Disk:
    cdef readonly:
        u64 read_speed
        tuple folders
        object io_thread
        volatile int busy
        volatile int cas_lock
        IoTask cache_task
    cdef :
        dict filename_map_path
        volatile ioTask* read_task
    def __init__(self):
        t=threading.Thread(target=start_read_thread, args=(self,), daemon=True)
        t.start()
        self.io_thread=t
    def __dealloc__(self):
        self.busy=2
        while(self.busy!=3): #等待Io线程结束
            continue
    cpdef read_file(self, filename):
        path = self.filename_get_path_in_main_thread(filename)
        if path is not None:
            fp = os.path.join(path, filename)
            cdef File file=File(fp)
            self.cache_task = file.get_read_task()
            self.submit_read(self.cache_task)
            return self.cas_task
        else:
            return None
    cpdef write_files_sync_in_io_thread(self, filename, path, data, list rs):
        cdef list errs=[], oks=[]
        try:
            fp = os.path.join(path, filename)
            if not os.path.exists(fp):
                pass
            else:
                raise FileExistsError
            f = open(fp, 'wb+', buffering=0)
            f.write(data)
            f.flush()
            #
            self.wait_get_lock(-1)
            self.filename_map_path[filename] = path
            self.release_lock()
            rs[0]=True
        except Exception as e:
            rs[0]=e
            errs.append(filename, e)
    cdef inline wait_get_lock(self, int v):
        # 乐观路径
        if self.cas_lock == 0: # 空闲
            self.cas_lock = v # 抢占
            if self.cas_lock == v: #成功
                return
        elif self.cas_lock!=v:
            while(1):
                while(self.cas_lock !=0 ): #被占用
                    continue
                self.cas_lock = v
                if self.cas_lock == v:
                    return
        else: #正在占用
            return
    cpdef filename_get_path_in_main_thread(self, filename):
        self.wait_get_lock(1)
        try:
            path = self.filename_map_path[filename]
        except KeyError:
            path=None
        self.release_lock()
        return path
    cdef inline release_lock(self):
        self.cas_lock=0
    cpdef IoTask try_get_read_result(self):
        if self.cache_task is not None and self.cache_task.task.success!=0:
            task=self.cache_task
            self.cache_task=None
            return task
        else:
            return None
    cpdef submit_read(self, IoTask task):
        self.read_task = &task.task
        self.busy = 1


cpdef start_read_thread(Disk disk):
    _start_read_thread(&disk.read_task, &disk.busy)

cdef _start_read_thread(volatile ioTask* task, volatile int* busy) nogil noexcept:
    cdef u64 read_size
    while (1):
        #等待任务
        while (busy[0]==0):
            continue
        # 退出
        if busy[0]==2:
            break
        # 执行任务
        if fseek(task.stream, 0, SEEK_SET) != 0:
            read_size = fread(task.data, 1, task.size, task.stream)
            task.success = task.size
        else:
            task.success = -1
        busy[0]=0
    busy[0]=3

cdef struct ioTask:
    FILE* stream
    u8* data
    u64 size
    volatile int success

cdef class IoTask:
    cdef readonly :
        File file
    cdef ioTask task
    @property
    def stat(self):
        if self.task.success==0:
            return None
        elif self.task.success==self.task.size:
            return True
        else:
            return False
    def __lt__(self,IoTask other):
        return self.task.size < other.task.size
    def __gt__(self,IoTask other):
        return self.task.size > other.task.size
    def __le__(self,IoTask other):
        return self.task.size <= other.task.size
    def __ge__(self,IoTask other):
        return self.task.size >= other.task.size
    def __dealloc__(self):
        self.file = None
        self.task.stream = NULL

class MyIoError(IOError):
    NotImplemented

cdef class File:
    cdef readonly :
        str filepath
        int fd
        u64 read_start, read_end
    cdef FILE* stream
    def __init__(self, str filepath):
        self.filepath = filepath
        self.write_ok = 0
        self.read_start = self.read_end = 0
        try:
            fd = os.open(filepath, os.O_RDWR)
        except Exception as e:
            raise RuntimeError
        cdef FILE* stream = fdopen(fd, c"rb+")
        setvbuf(stream, NULL, 0)
        if stream != NULL:
            self.stream = stream
        else:
            raise RuntimeError
    cpdef write_sync(self, const u8[:] data):
        cdef bint r
        cdef int* ptr = &self.write_stat
        cdef FILE* stream=self.stream
        cdef u8* datap=&data[0]
        cdef u64 size = data.nbytes
        with nogil:
            if fseek(stream, 0, SEEK_SET) == 0:
                if fwrite(datap, size, 1, stream) == size:
                    fsync_stream(self)
                else:
                    raise IOError
            else:
                raise IOError
    cpdef read_sync(self, const u8[:] data):

    cpdef u64 filelen(self):
        return os.path.getsize(self.filepath)
    cpdef IoTask get_read_task(self):
        cdef IoTask task= IoTask()
        cdef u64 size = self.filelen()
        cdef u8* data = py_malloc(size)
        task.file = self
        task.task = ioTask(self.stream, data, 0, size, 0)
        return task
    def __dealloc__(self):
        if self.stream != NULL:
            fclose(self.stream)
            self.stream=NULL
            self.fd = -1
            self.filepath = None

cpdef write_files_sync( filenames_and_datas, disk_and_paths):

cpdef write_file_sync(filename, data, disks_and_paths):
    cdef list files = [], disks = [], rs = [], r, rs1=[], success=[], faults = []
    cdef tuple arg
    cdef set rs = set()
    cdef u64 i, l = len(disks_and_paths)
    for disk, path in disks_and_paths:
        fp = os.path.join(path, filename)
        files.append((disk, (filename, path, data), [None]))
    for disk, arg, r in files:
        t = threading.Thread(target=disk.write_files_sync_in_io_thread, args=arg, daemon=True)
        t.start()
        rs.append((r, arg[1]))
        #
    while (rs):
        for rpath in rs:
            r, path = rpath
            stat = r[0]
            if stat is None:
                rs1.append(stat)
            if stat is True:
                success.append(path)
            else:
                faults.append((path, stat))
        rs.clear()
        rs, rs1 = rs1, rs
    return success, faults




#-----------------------------------------------------------------------------------------------------------------------

cdef class IterReadTasks:
    cdef readonly:
        ReadTasks waits
    cdef :
        set reading
        list pops, success, faults
    def __init__(self, ReadTasks waits):
        self.waits=waits
        self.reading=set()
        self.pops=[]
    def __next__(self):
        cdef IoTask task
        if self.waits :
            self.reading += self.waits.submit_read()
        if self.reading:
            for task in self.reading:
                if task.stat is True:
                    self.pops.append(task)
                    filename = os.path.basename(task.file.filepath)
                    data = BufferStealer().set(task.task.data, task.task.size)
                    self.success.append((filename, data))
                elif task.stat is False:
                    self.pops.append(task)
                    self.faults.append(filename)
            r = ( tuple(self.success), tuple(self.faults) )
            self.success.clear()
            self.faults.clear()
            if len(self.pops):
                for tsk in self.pops:
                    self.reading.pop(tsk)
                self.pops.clear()
            return r
        else:
            return None

cdef class ReadTasks:
    cdef readonly :
        tuple disks
    cdef:
        set wait_read_filenames
    def __init__(self, disks, filenames):
        self.disks=disks
        self.wait_read_filenames = filenames
    def __bool__(self):
        return len(self.wait_read_filenames)>0
    def __iter__(self):
        return IterReadTasks(self)
    cpdef submit_read(self):
        cdef Disk disk
        cdef File file
        cdef IoTask task
        cdef list tasks=[]
        for disk in self.disks:
            if disk.busy==0:
                for filename in self.wait_read_filenames:
                    task = disk.read_file(filename)
                    if task is not None:
                        tasks.append(task)
                        self.wait_read_filenames.remove(filename)
                        break
                    else: continue
            else:
                continue
        return tasks


cpdef read_files(disks_and_filenames, dict filename_map_callback):
    cdef set all_disks=set(), filenames=set(), reading=set()
    cdef list pops=[], accepts
    cdef IoTask task
    cdef bint task_pop
    for disks, filenames in disks_and_filenames:
        all_disks.union(disks)
        filenames.union(filenames)
    cdef ReadTasks tasks = ReadTasks(all_disks, filenames)
    accepts = tasks.submit_read()
    reading += accepts
    #
    cdef list success=[], faults=[], errs=[]
    while(1):
        for task in reading:
            stat = task.stat
            if stat is True:
                pops.append(task)
                filename = os.path.basename(task.file.filepath)
                call = filename_map_callback[filename]
                data = BufferStealer().set(task.task.data, task.task.size)
                try:
                    r = call(data)
                except Exception as e:
                    errs.append((filename, data, e))
                else:
                    success.append((filename, r))
            elif stat is False:
                reading.remove(task)
                pops.append(task)
                faults.append(filename)
            else:
                continue
        if pops:
            reading += tasks.submit_read()
            for task in pops:
                reading.pop(task)
            reading.clear()
            if tasks.wait_read_filenames:
                continue
            else:
                break
    return success, faults, errs



cpdef read_files_raise(disks_and_filenames, dict filename_map_callback):
    success, faults = read_files(disks_and_filenames)
    if faults:
        raise IOError
    return success


